1
|
|
|
import process from 'process'; |
2
|
|
|
import fs from 'fs'; |
3
|
|
|
import { promisify, props, resolve } from 'bluebird'; |
4
|
|
|
import { |
5
|
|
|
T, |
6
|
|
|
__, |
7
|
|
|
assoc, |
8
|
|
|
concat, |
9
|
|
|
cond, |
10
|
|
|
contains, |
11
|
|
|
curry, |
12
|
|
|
filter as filterWhere, |
13
|
|
|
has, |
14
|
|
|
keys, |
15
|
|
|
map, |
16
|
|
|
mapObjIndexed, |
17
|
|
|
merge, |
18
|
|
|
pathEq, |
19
|
|
|
reduce, |
20
|
|
|
toPairs, |
21
|
|
|
when |
22
|
|
|
} from 'ramda'; |
23
|
|
|
import { cyan, green, red, yellow } from 'colors/safe'; |
24
|
|
|
import { createPromptModule } from 'inquirer'; |
25
|
|
|
import { validator, filter } from './types'; |
26
|
|
|
import getAutocompleteSources, { compileClosure } from './autocomplete'; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Emits an info message to stdout |
30
|
|
|
* |
31
|
|
|
* @param {String} message |
32
|
|
|
* @return {Promise} |
33
|
|
|
*/ |
34
|
|
|
export const emitInfo = concat(' ℹ Info: ') & cyan & console.log & resolve; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Emits a warning to stdout |
38
|
|
|
* |
39
|
|
|
* @param {String} message |
40
|
|
|
* @return {Promise} |
41
|
|
|
*/ |
42
|
|
|
export const emitWarning = concat(' ⚠ Warning: ') & yellow & console.log & resolve; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Emits an error to stdout |
46
|
|
|
* |
47
|
|
|
* @param {String} message |
48
|
|
|
* @return {Promise} |
49
|
|
|
*/ |
50
|
|
|
export const emitError = concat(' ✗ Error: ') & red & console.log & resolve; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Emits a success message |
54
|
|
|
* |
55
|
|
|
* @param {String} message |
56
|
|
|
* @return {Promise} |
57
|
|
|
*/ |
58
|
|
|
export const emitSuccess = concat(' ✔ Success: ') & green & console.log & resolve; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Renames the keys of an object |
62
|
|
|
* |
63
|
|
|
* @sig {a: b} -> {a: *} -> {b: *} |
64
|
|
|
*/ |
65
|
|
|
const renameKeys = curry((keysMap, obj) => reduce((acc, key) => |
66
|
|
|
assoc(keysMap[key] || key, obj[key], acc), {}, keys(obj))); |
67
|
|
|
|
68
|
|
|
const objectToChoices = toPairs & map(([value, name]) => ({ name, value })); |
69
|
|
|
|
70
|
|
|
export const components = { |
71
|
|
|
Calendar: ~{ |
72
|
|
|
type: 'datetime', |
73
|
|
|
format: ['m', '/', 'd', '/', 'yy'], |
74
|
|
|
filter: filter.Calendar }, |
75
|
|
|
Char: ({ length }) => ({ filter: filter.Char(length) }), |
76
|
|
|
Checkbox: ~{ type: 'confirm' }, |
77
|
|
|
Color: ~{ |
78
|
|
|
type: 'chalk-pipe', |
79
|
|
|
validate: validator.Color }, |
80
|
|
|
DoubleRange: ({ from, to }) => ({ |
81
|
|
|
filter: filter.Double, |
82
|
|
|
validate: validator.Range(from, to) }), |
83
|
|
|
DateTime: ~{ type: 'datetime' }, |
84
|
|
|
Double: ~{ validate: validator.Double, filter: filter.Double }, |
85
|
|
|
Email: ~{ validate: validator.Email }, |
86
|
|
|
Integer: ~{ validate: validator.Integer, filter: filter.Integer }, |
87
|
|
|
IntegerRange: ({ from, to }) => ({ |
88
|
|
|
filter: filter.Integer, |
89
|
|
|
validate: validator.Range(from, to) }), |
90
|
|
|
IntegerMultiRange: ({ from, to }) => ({ |
91
|
|
|
filter: filter.IntegerMultiRange, |
92
|
|
|
validate: validator.IntegerMultiRange(from, to) }), |
93
|
|
|
Natural: ~{ validate: validator.Natural, filter: filter.Integer }, |
94
|
|
|
OneOf: ({ values }) => ({ |
95
|
|
|
type: 'list', |
96
|
|
|
choices: values, |
97
|
|
|
validate: validator.OneOf(values) }), |
98
|
|
|
String: ~{ type: 'input' }, |
99
|
|
|
Url: ~{ validate: validator.Url }, |
100
|
|
|
Money: ~{ validate: validator.Money, filter: filter.Money }, |
101
|
|
|
SelectBox: ({ values }) => ({ |
102
|
|
|
type: 'list', |
103
|
|
|
choices: objectToChoices(values) }), |
104
|
|
|
MultiSelectBox: ({ values }) => ({ |
105
|
|
|
type: 'checkbox', |
106
|
|
|
choices: objectToChoices(values) }), |
107
|
|
|
File: ~{ type: 'filePath', basePath: process.cwd() } |
108
|
|
|
}; |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Custom autocomplete component |
112
|
|
|
* |
113
|
|
|
* @param {String} name |
114
|
|
|
* @param {String} source |
115
|
|
|
* @return {Function} |
116
|
|
|
*/ |
117
|
|
|
const getAutocompleteComponent = (name, source) => { |
118
|
|
|
if (!source) { |
119
|
|
|
throw new Error(`aren't you missing 'autocomplete/${name}.js'?`); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
return ~{ type: 'autocomplete', source: compileClosure(name, source) }; |
123
|
|
|
}; |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Converts a Rung CLI question object to an Inquirer question object |
127
|
|
|
* |
128
|
|
|
* @author Marcelo Haskell Camargo |
129
|
|
|
* @param {String[]} sources |
130
|
|
|
* @param {String} name |
|
|
|
|
131
|
|
|
* @param {Object} config |
|
|
|
|
132
|
|
|
* @return {Object} |
133
|
|
|
*/ |
134
|
|
|
const toInquirerQuestion = curry((sources, [name, config]) => { |
135
|
|
|
const component = components |
136
|
|
|
| cond([ |
137
|
|
|
[~(config.type.name === 'AutoComplete'), ~getAutocompleteComponent(name, sources[name])], |
138
|
|
|
[has(config.type.name), _[config.type.name]], |
|
|
|
|
139
|
|
|
[T, _.String] |
140
|
|
|
]); |
141
|
|
|
|
142
|
|
|
return merge(config |
143
|
|
|
| renameKeys({ description: 'message' }) |
144
|
|
|
| merge(__, { name }), component(config.type)); |
145
|
|
|
}); |
146
|
|
|
|
147
|
|
|
const readFile = promisify(fs.readFile); |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Opens the provided files and returns them as node buffers |
151
|
|
|
* |
152
|
|
|
* @param {String[]} fields |
153
|
|
|
* @param {Object} answers |
|
|
|
|
154
|
|
|
* @return {Promise} |
155
|
|
|
*/ |
156
|
|
|
const openFiles = fields => |
157
|
|
|
mapObjIndexed((value, param) => value |
158
|
|
|
| when(~contains(param, fields), concat(process.cwd() + '/') & readFile)) |
159
|
|
|
& props; |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Returns the pure JS values from received questions that will be answered |
163
|
|
|
* |
164
|
|
|
* @author Marcelo Haskell Camargo |
165
|
|
|
* @param {Object} questions |
166
|
|
|
* @return {Promise} answers for the questions by key |
167
|
|
|
*/ |
168
|
|
|
export function ask(questions) { |
169
|
|
|
const DatePickerPrompt = require('inquirer-datepicker-prompt'); |
170
|
|
|
const ChalkPipe = require('inquirer-chalk-pipe'); |
171
|
|
|
const AutocompletePrompt = require('inquirer-autocomplete-prompt'); |
172
|
|
|
const FilePath = require('inquirer-file-path'); |
173
|
|
|
const fileFields = questions |
174
|
|
|
| filterWhere(pathEq(['type', 'name'], 'File')) |
175
|
|
|
| keys; |
176
|
|
|
|
177
|
|
|
const prompt = createPromptModule(); |
178
|
|
|
prompt.registerPrompt('datetime', DatePickerPrompt); |
179
|
|
|
prompt.registerPrompt('chalk-pipe', ChalkPipe); |
180
|
|
|
prompt.registerPrompt('autocomplete', AutocompletePrompt); |
181
|
|
|
prompt.registerPrompt('filePath', FilePath); |
182
|
|
|
return getAutocompleteSources() |
183
|
|
|
.then(autocompleteSources => questions |
184
|
|
|
| toPairs |
185
|
|
|
| map(toInquirerQuestion(autocompleteSources)) |
186
|
|
|
| prompt) |
187
|
|
|
.then(openFiles(fileFields)); |
188
|
|
|
} |
189
|
|
|
|